﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using Skugo.Shared;
using System.Windows;

namespace Skugo.Core
{
	/// <summary>
    /// A plugin is a core class that is implemented to receive messages.
	/// </summary>
    public abstract class Plugin : IDisposable
	{
		/// <summary>
        /// Store the guid on the plugin.
		/// </summary>
        internal Int64 InstanceGuid;

		/// <summary>
        /// The client that we use to communicate.
		/// </summary>
        public ProjectClient Client;

        internal void InternalInitialize(Int64 guid, ProjectClient client, BinaryReader incoming)
        {
            // Store the instance guid for this plugin, and the associated client
			this.InstanceGuid = guid;
            this.Client = client;

            // Initialize the user's plugin
            this.Initialize(incoming);
        }

        /// <summary>
        /// Initializes the plugin (like a constructor, however you can send messages in intialize).
        /// </summary>
        protected virtual void Initialize(BinaryReader incoming)
        {
        }

        // Occurs when the plugin receives data from its external counterpart
        public void Receive(byte[] data, PacketType type)
        {
            // If the packet type is just a message, then it is intended for the user
            if (type == PacketType.Message)
            {
                this.OnReceive(data);
            }
            else
            {
                // Otherwise, we had no idea what it was
                throw new Exception("Unknown packet type encountered");
            }
        }

		// Occurs when the plugin receives data from its external counterpart
		protected abstract void OnReceive(byte[] data);
		
		// Called when we dispose of the plugin from the other end
		public abstract void Dispose();

		// Send data to your remote counterpart
        public void Send(Byte[] data)
		{
            this.Send(data, PacketType.Message);
		}

        // Send data to your remote counterpart
        private void Send(Byte[] data, PacketType type)
        {
            // Check if we haven't been initialized yet
            if (this.Client == null)
            {
                throw new Exception("Cannot send messages in your constructor. Override the 'Initialize' function and send messages there.");
            }

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter writer = new BinaryWriter(stream);

            // Write out the packet data
            writer.Write((UInt32)data.Length);
            writer.Write((Byte)type);
            writer.Write((Int64)this.InstanceGuid);

            // Write the provided data
            writer.Write(data);

            // Send the packet out
            this.Client.Write(stream.ToArray());
        }

        /// <summary>
        /// Call this when an item is dropped on your plugin. This will
        /// handle the serialization and sending of the event to the remote side.
        /// </summary>
        /// <param name="data">The data you want to send with the event.</param>
        /// <param name="e">The drag drop event which stores data.</param>
        protected void SendDragDrop(Byte[] data, DragEventArgs e)
        {
            // Grab the plugin data
            var dragData = (PluginDragData)e.Data.GetData(PluginDragData.Format);

            // Only send the message if the drag data is valid
            if (dragData != null)
            {
                // Create a memory stream to write data to
                MemoryStream stream = new MemoryStream();

                // Create a binary writer to write the data
                BinaryWriter writer = new BinaryWriter(stream);

                // Write out who the orignal data was from
                writer.Write(dragData.Source.InstanceGuid);

                // Write out the rest of the data
                writer.Write(dragData.Data.Length);
                writer.Write(dragData.Data);

                // Now write out the user's data
                writer.Write(data);

                // Send the drag drop event to our remote counterpart, but let
                // the source plugin be responsible for deserializing the data
                this.Send(stream.ToArray(), PacketType.DragDrop);
            }
        }
	}

	// A class that imports and instantiates plugins
	public class PluginFactory
	{
		// A thread that we use to load all the plugins
        private static Thread LoadThread;

        /// <summary>
        /// Get the directory path to the plugin location.
        /// </summary>
        public static String PluginPath
        {
            get { return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); }
        }

		// Load all the available plugins
		public static void LoadPlugins()
		{
			// Create the thread
			LoadThread = new Thread(LoadPluginsThread);

			// Start loading plugins
			LoadThread.Start();
		}

		// A thread function for loading plugins
		private static void LoadPluginsThread()
		{
			// Get the plugin directory
            String pluginDir = PluginPath;

			// Get all the plugin files
			var pluginFiles = Directory.EnumerateFiles(pluginDir, "*.dll");

			// Loop through all the plugin files
			foreach (String pluginFile in pluginFiles)
			{
				// Attempt to load each assembly
				//Assembly.LoadFile(pluginFile);
                Assembly.LoadFrom(pluginFile);
			}
		}

		// Create a plugin
		public static void CreatePlugin(String typeName, Int64 guid, ProjectClient client, BinaryReader incoming)
		{
			// Wait until we loaded all the assemblies
			LoadThread.Join();

			// Perform a query to get all types that are plugin types
			var types = AppDomain.CurrentDomain.GetAssemblies().ToList()
				.SelectMany(s => s.GetTypes())
				.Where(p => typeof(Plugin).IsAssignableFrom(p));

			// Store the found type of the plugin
			Type foundPluginType = null;

			// Loop through all the plugin types
			foreach (Type currentPluginType in types)
			{
				// If we found a matching name...
				if (currentPluginType.FullName == typeName)
				{
					// Set the found plugin type and break out early
					foundPluginType = currentPluginType;
					break;
				}
			}

			// If we did not find the type we wish to instantiate...
			if (foundPluginType == null)
			{
                // Error out
                throw new Exception(String.Format("Plugin type not found '{0}'", typeName));
			}

            // Create the plugin!
            // We use this line to create the plugin, because for some reason the below line will cause
            // Generic Container Visualizers to not work properly when containing our own defined types
            Plugin plugin = (Plugin)Activator.CreateInstance(foundPluginType);
            plugin.InternalInitialize(guid, client, incoming);

			// If for some reason we did not create the plugin...
			if (plugin == null)
			{
				// Error out
                throw new Exception(String.Format("Could not instantiate plugin of type '{0}'", typeName));
			}

			// Add the plugin to the map
            client.Plugins.AddPlugin(plugin);
		}
	}

    public class PluginContainer
    {
        // Hold all the existing plugins
        private Dictionary<Int64, Plugin> Plugins = new Dictionary<Int64, Plugin>();

        public void AddPlugin(Plugin plugin)
        {
            this.Plugins[plugin.InstanceGuid] = plugin;
        }

        // Get a plugin by Guid
        public Plugin GetPlugin(Int64 guid)
        {
            return this.Plugins[guid];
        }

        /// <summary>
        /// Destroy a plugin by guid.
        /// </summary>
        /// <param name="guid">The guid of the plugin we want to destroy.</param>
        public void Destroy(Int64 guid)
        {
            // Call the plugin's own disposal function
            this.Plugins[guid].Dispose();

            // Remove the plugin from the plugins list
            this.Plugins.Remove(guid);
        }

        /// <summary>
        /// Destroy all existing plugins. This is typically called upon a disconnect.
        /// </summary>
        public void DestroyAll()
        {
            // Loop through all existing plugins
            foreach (var plugin in this.Plugins.Values)
            {
                // Dispose the current plugin
                plugin.Dispose();
            }

            // Clear out all the values
            this.Plugins = null;
        }
    }
}
